package de.uni_stuttgart.ipvs.ids.nameserver;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;

/**
 * Client for communicating with Nameserver instances. Able to resolve names to addresses using the recursive or iterative method.
 * @author Team 3/4 - Elkhan Dadashov, Dhaval A Shah, Patrick Stortz
 */
public class Client {

	/** The timeout in milliseconds for socket connect() and receive() calls. */
	private static int TIMEOUT = 5000;
	/** The name identifying this instance */
	private String mName;

	/** @return the name identifying this instance. */
	public String getName() {
		return this.mName;
	}

	/** Creates a Client instance. */
	public Client(){
		// try to get the IP address of this machine
		try {
			InetAddress ip = InetAddress.getLocalHost();
			this.mName = "Client @ " + ip.getHostAddress();
		} catch (UnknownHostException e) {
			this.mName = "Client";
		}
	}

	/**
	 * Resolves a name to an address using the recursive method.
	 * @param name the name to resolve
	 * @param serverAddress the address of the name server to ask for the resolution (root name server)
	 * @param serverPort the port of the name server to ask for the resolution (root name server)
	 * @return the reply of the name server (resolved address or error message)
	 */
	public String startRecursiveNameResolution(String name, String serverAddress, int serverPort) {

		System.out.println();
		System.out.println("------------------------------------------------------------------------------------");// DEBUG LOG OUTPUT
		System.out.println(this.getName() + " | Starting recursive resolution by using nameserver at " + serverAddress + ":" + serverPort + " for address of " + name);// DEBUG LOG OUTPUT

		String address = null;
		try {
			// connect to the server using a TCP socket
			Socket clientTCPSocket = new Socket();
			clientTCPSocket.setSoTimeout(5000);
			clientTCPSocket.connect(new InetSocketAddress(serverAddress, serverPort),TIMEOUT);

			try {
				// open the streams
				PrintWriter outputWriter = new PrintWriter(clientTCPSocket.getOutputStream(), true);
				BufferedReader inputReader = new BufferedReader(new InputStreamReader(clientTCPSocket.getInputStream()));

				// send request to the server
				outputWriter.println(name);

				// wait for the response of the server
				address = inputReader.readLine().trim();

				// close the streams
				outputWriter.close();
				inputReader.close();
			} catch (Exception e) {
				// error occurred during the communication
				System.out.println(this.getName() + " | Error! The address for name " + name + " could not get resovled! (" + e.getMessage() + ")");// DEBUG LOG OUTPUT
				address = "?.?.?.? (Communication error occurred)";
			}

			// close socket
			clientTCPSocket.close();

		} catch (IOException e) {
			// error occurred during the connection
			System.out.println(this.getName() + " | Error! Connection to " + serverAddress + ":" + + serverPort + " could not be established! (" + e.getMessage() + ")");// DEBUG LOG OUTPUT
			address = "?.?.?.? (Server " + serverAddress + ":" + serverPort + " not reachable)";
		}

		System.out.println(this.getName() + " | The address for name " + name + " is " + address + "");// DEBUG LOG OUTPUT
		System.out.println("------------------------------------------------------------------------------------");// DEBUG LOG OUTPUT
		System.out.println();
		return address;
	}

	/**
	 * Resolves a name to an address using the iterative method.
	 * @param name the name to resolve
	 * @param serverAddress the address of the name server to start with (root name server)
	 * @param serverPort the port of the name server to start with (root name server)
	 * @return the reply of the name server which finally resolved the address (resolved address or error message)
	 */
	public String startIterativeNameResolution(String name, String serverAddress, int serverPort) {
		System.out.println();
		System.out.println("------------------------------------------------------------------------------------");// DEBUG LOG OUTPUT
		System.out.println(this.getName() + " | Starting iterative resolution by using nameserver at " + serverAddress + ":" + serverPort + " for address of " + name);// DEBUG LOG OUTPUT
		
		String address = null;
		
		DatagramSocket socket = null;
		try {
			// create a socket
			socket = new DatagramSocket();
			socket.setSoTimeout(5000);
		} catch (SocketException e) {
			// error occurred during the communication
			System.out.println(this.getName() + " | Error! Could not establish a connection! (" + e.getMessage() + ")");// DEBUG LOG OUTPUT
			address = "?.?.?.? (Socket error occurred)";		
			return address;
		}

		// loop until we got the final address
		boolean resoluting = true;
		String originalName = name;
		while(resoluting){
			try {
				System.out.println(this.getName() + " | Asking name server at " + serverAddress + ":" + serverPort + " for address of " + name);// DEBUG LOG OUTPUT
				// create request packet
				DatagramPacket requestPacket = new DatagramPacket(name.getBytes(), name.length());
				requestPacket.setAddress(InetAddress.getByName(serverAddress));
				requestPacket.setPort(serverPort);
				// send the request packet
				socket.send(requestPacket);
				// wait for the reply
				byte[] data = new byte[256];
				DatagramPacket packet = new DatagramPacket(data, data.length);
				socket.receive(packet);
				// process the reply
				String dataReceived = new String(packet.getData(), 0, packet.getLength()).trim();
				System.out.println(getName() + " | Got following reply from name server at " + serverAddress + ":" + serverPort + ": " + dataReceived);// DEBUG LOG OUTPUT
				// check if we got a resolution error
				if(dataReceived.contains("?")){
					// resolution error occurred on one of the name servers, save error and end the loop
					address = dataReceived;
					resoluting=false;
				}else{
					// no resolution error, check if we got another name server
					if(dataReceived.contains(":")){
						// we got another name server, remove highest domain
						name = name.substring(0, name.lastIndexOf("."));
						// set the new name server to proceed in the iteration
						String[] newServer = dataReceived.split(":");
						serverAddress = newServer[0];
						serverPort = Integer.decode(newServer[1]);
					}else{
						// we got the final address, save the address and end the loop
						address = dataReceived;
						resoluting=false;
					}
				}
			} catch (Exception e) {
				// error occurred during the communication
				System.out.println(this.getName() + " | Error! The address for name " + originalName + " could not get resovled! (" + e.getMessage() + ")");// DEBUG LOG OUTPUT
				address = "?.?.?.? (Communication error occurred)";		
				resoluting=false;
			}
		}

		// close the socket
		socket.close();

		System.out.println(this.getName() + " | The address for name " + originalName + " is " + address + "");// DEBUG LOG OUTPUT
		System.out.println("------------------------------------------------------------------------------------");// DEBUG LOG OUTPUT
		System.out.println();
		return address;
	}
}
